NetCore 之 log4net 实战
上一篇主要详细介绍log4net相关的一些配置项,本章意在从实战角度详解log4net在NetCore中使用。
1、创建Netcore consol application 通过Nuget package安装log4net(Microsoft.Extensions.Logging.Log4Net.AspNetCore), Hosting(Microsoft.Extensions.Hosting)及DI(Microsoft.Extensions.DependencyInjection),除了log4net 安装DI及Hosting旨在通过DI方式使用ILog。
2、创建log4net.config 文件,设置Copy to Output Directory:Copy Always
<log4net debug="false" update="Merge" threshold="ALL"> <appender name="RollingFile" type="log4net.Appender.RollingFileAppender"> <lockingModel type="log4net.Appender.FileAppender+MinimalLock" /> <file value="log4netdemo.log"/> <appendToFile value = "true"/> <rollingStyle value="Size"/> <maximumFileSize value = "10KB" /> <maxSizeRollbackups value = "2"/> <staticLogFileName value = "true"/> <!--<layout type ="Log4netDemo.ELKWebLayout, Log4netDemo"/>--> <layout type="Log4netDemo.DemoPatterLayout,Log4netDemo"> <param name="ConversionPattern" value="%d [%t] %5level %clientIP %userName %logger.%method [%line] -MESSAGE: %message %newline %exception"/> </layout> </appender> <root> <level value="All" /> <appender-ref ref="RollingFile"/> </root> </log4net>
- 配置详解:file 指定日志输出文件地址;appendToFile指定以追加的方式写入日志;rollingStyle指定为size;maximumFileSize最大文件大小10kb;maxSizeRollbackups保留日志文件个数2个;staticLogFileName日志文件为静态命名,rollback的文件会添加后缀.1,.2
- ConverionPattern value中formart即日志输出到文件中的格式,其中 %clientIP %userName 为自定义PatternConvert.用于将自定义信息以patternLayout的方式输出到日志,具体设置稍后介绍。
创建一个LogService 用于测试Logger
public class LogService { private static ILog _logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); private static ILog _loggerInfo = LogManager.GetLogger(Assembly.GetEntryAssembly(), "SpecifyLogger"); //private readonly ILogger _logger; //public LogService(ILogger<LogService> logger) //{ // _logger = logger; //} public void Run() { _logger.Info($"The application start: {DateTime.Now}."); } public void RunV2() { for (int i = 0; i < 200; i++) { _loggerInfo.Info($"The application start: {DateTime.Now}."); _logger.Error($"The application start: {DateTime.Now}."); } } public void RunV3() { LogicalThreadContext.Properties[LoggerProperty.Ip] = "127.0.0.1"; LogicalThreadContext.Properties[LoggerProperty.UserName] = "panda"; LogicalThreadContext.Properties[LoggerProperty.Company] = "zoo"; LogicalThreadContext.Properties[LoggerProperty.Department] = "suckler"; _logger.Info($"Test for the customize elk layout."); } public void RunV4() { for (int i = 0; i < 200; i++) { _logger.Info($"This is root logger - {i}."); } _logger.Fatal($"This is FATAL level error."); _loggerInfo.Info($"This is sub logger"); } }
- 可以通过构造函数的方式依赖注入ILogger
- 另一种方式是通过LogManager同样构建ILog
- 如果配置文件中配置了多个Logger,可以通过指定logger的方式获取到指定Logger,比如LogManager.GetLogger(Assembly.GetEntryAssembly(), "SpecifyLogger");获取SpecifyLogger对象。
将LogService注入到容器中及配置log4net
using Log4netDemo; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; var builder = new HostBuilder() .ConfigureServices((hostContext, services) => { services.AddTransient<LogService>(); }) .ConfigureLogging(logBuilder => { logBuilder.AddLog4Net("log4net.config", true); }).UseConsoleLifetime(); var host = builder.Build(); var logService = host.Services.GetRequiredService<LogService>(); logService.RunV4(); host.Run();
启动程序可以看到生成了一个log4netdemo.log文件,并且日志成功写入
以上只是log4net的基本使用,下面介绍log4net的进阶操作
1、如果log4net build-in PatternLayout format不能满足开发需求,比如想要将Ip,userName,Company,department等信息通过PatternLayout format记录到日志怎么做,这里就需要自定义PatternConvert,下面示例自定义了ClientIPPatternConvert及UserNamePatternConvert
public class ClientIPPatternConvert : PatternLayoutConverter
{
protected override void Convert(TextWriter writer, LoggingEvent loggingEvent)
{
var props = loggingEvent.GetProperties();
if (props?[LoggerProperty.Ip] != null)
writer.Write($"IP:{props?[LoggerProperty.Ip].ToString()}");
}
}
public class UserNamePatternConvert : PatternLayoutConverter { protected override void Convert(TextWriter writer, LoggingEvent loggingEvent) { var props = loggingEvent.GetProperties(); if (props?[LoggerProperty.UserName] != null) writer.Write($"user name:{props?[LoggerProperty.UserName]}"); } }
接下来需要将自定义的PatternConverter加到log4net的PatternLayout中,创建一个DemoPatternLayout继承自 log4net.Layout.PatternLayout
public class DemoPatterLayout: log4net.Layout.PatternLayout { public DemoPatterLayout() { this.AddConverter("clientIP", typeof(ClientIPPatternConvert)); this.AddConverter("userName", typeof(UserNamePatternConvert)); } }
修改log4net.config中layout配置,指定type="Log4netDemo.DemoPatterLayout,Log4netDemo",使用自定义的PatternConvert格式化字符串%clientIP, %userName
<layout type="Log4netDemo.DemoPatterLayout,Log4netDemo"> <param name="ConversionPattern" value="%d [%t] %5level %clientIP %userName %logger.%method [%line] -MESSAGE: %message %newline %exception"/> </layout>
在LogService中向log4net添加IP及UserName属性值
LogicalThreadContext.Properties[LoggerProperty.Ip] = "127.0.0.1"; LogicalThreadContext.Properties[LoggerProperty.UserName] = "panda";
运行结果
2、自定义layout,某些情况下不想用PatternConverter format或者想对接ElasticSearch,基于以上case可以定义Layout
创建AbstractLayout基类继承自LayoutSkeleton
public abstract class AbstractLayout : LayoutSkeleton { protected static JsonSerializerOptions _defaultJsonOptions => new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; protected abstract string _role { get;} public AbstractLayout() { IgnoresException = false; } public override void ActivateOptions() { } public override void Format(TextWriter writer, LoggingEvent loggingEvent) { if (!(loggingEvent.Level == Level.Trace)) DoFormat(writer, loggingEvent); } public abstract void DoFormat(TextWriter writer, LoggingEvent loggingEvent); }
BaseLogger的实体类
public class LoggerMessage { private string _defaultDateFormat => "yyyy-MM-ddTHH:mm:ss.fffZ"; public LoggerMessage(LoggingEvent loggingEvent, string role) { Time = loggingEvent.TimeStampUtc.ToString(_defaultDateFormat); Level = loggingEvent.Level.Name; Thread = loggingEvent.ThreadName; Logger = loggingEvent.LoggerName; Message = loggingEvent.RenderedMessage; Role = role; } public String? Time { get; set; } public String? Level { get; set; } public String? Thread { get; set; } public String? Logger { get; set; } public String? Message { get; set; } public String? Role { get; set; } }
定义web模块Layout(可以基于不同的模块或不同的image根据需要创建多个Layout)
public class ELKWebLayout : AbstractLayout { protected override string _role { get => "demo-web"; } public override void DoFormat(TextWriter writer, LoggingEvent loggingEvent) { var props = loggingEvent.GetProperties(); var webLogger = new WebLoggerMessage(loggingEvent, _role); webLogger.Ip = props?[LoggerProperty.Ip]?.ToString() ?? ""; webLogger.Company = props?[LoggerProperty.Company]?.ToString() ?? ""; webLogger.Department = props?[LoggerProperty.Department]?.ToString() ?? ""; writer.WriteLine(JsonSerializer.Serialize(webLogger, _defaultJsonOptions)); } }
WebLogger实体类
public class WebLoggerMessage : LoggerMessage { public WebLoggerMessage(LoggingEvent loggingEvent, string role) : base(loggingEvent, role) { } public String? Ip { get; set; } public String? Company { get; set; } public String? Department { get; set; } }
在log4net.config中指定自定义的layout
<layout type ="Log4netDemo.ELKWebLayout, Log4netDemo"/>
给log4net自定义属性赋值
LogicalThreadContext.Properties[LoggerProperty.Ip] = "127.0.0.1"; LogicalThreadContext.Properties[LoggerProperty.UserName] = "panda";
运行结果
3、某些时候想将一些重要的日志信息单独输出到一个文件,而不包含在公共的日志文件中,基于以上需求需要创建多个Appender及Logger,通过Logger中additivity属性决定是否将sub logger信息包含到root logger中,如下配置
<log4net debug="false" update="Merge" threshold="ALL"> <appender name="RootRoolingFileAppender" type="log4net.Appender.RollingFileAppender"> <lockingModel type="log4net.Appender.FileAppender+MinimalLock" /> <file value="log4netdemo.log"/> <appendToFile value = "true"/> <rollingStyle value="Size"/> <maximumFileSize value = "10KB" /> <maxSizeRollbackups value = "2"/> <staticLogFileName value = "true"/> <layout type="log4net.Layout.PatternLayout"> <param name="ConversionPattern" value="%d [%t] %5level %logger.%method [%line] -MESSAGE: %message%newline %exception"/> </layout> </appender> <appender name="SubRoolingFileAppender" type="log4net.Appender.RollingFileAppender"> <lockingModel type="log4net.Appender.FileAppender+MinimalLock" /> <file value="log4netdemo.sub.log"/> <appendToFile value = "true"/> <rollingStyle value="Size"/> <maximumFileSize value = "10KB" /> <maxSizeRollbackups value = "2"/> <staticLogFileName value = "true"/> <layout type="log4net.Layout.PatternLayout"> <param name="ConversionPattern" value="%d [%t] %5level %logger.%method [%line] -MESSAGE: %message%newline %exception"/> </layout> </appender> <root> <level value="All" /> <appender-ref ref="RootRoolingFileAppender"/> </root> <logger name="SpecifyLogger" additivity="false"> <level value="All" /> <appender-ref ref="SubRoolingFileAppender"/> </logger> </log4net>
在LogService中通过Logger name指定使用哪个Logger
private static ILog _logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); private static ILog _loggerV2 = LogManager.GetLogger(Assembly.GetEntryAssembly(), "SpecifyLogger");
启动程序生成两个log文件
查看log信息发现log4netdemo.log中不会记录SpecifyLogger 记录的日志信息,因为在创建SpecifyLogger 时设置additivity="false"
4、有时可能还有需求,不同Appender需要记录不同level 日志信息,比如FATAL level的信息需要单独一个文件记录,或者INFO-ERROR level range的日志信息需要记录到某个文件中,基于以上需求可以在Appender中添加filter实现
a.只会记录FATAL level的日志信息
<filter type="log4net.Filter.LevelMatchFilter"> <levelToMatch value ="FATAL"/> </filter> <filter type="log4net.Filter.DenyAllFilter"/>
b.记录Level范围在INFO和ERROR之间的日志信息
<filter type="log4net.Filter.LevelMatchFilter"> <levelMax value ="ERROR"/> <levelMin value ="INFO"/> </filter> <filter type="log4net.Filter.DenyAllFilter"/>
Filter执行原理是顺序执行,如果满足上一个Filter则日志会被记录;如果不满足上一个Filter会继续匹配下一个Filter。
最后附上完整的代码结构
如有任何问题,请留言,谢谢!!!