NetCore 之 log4net 实战

发布时间 2023-09-13 14:01:00作者: 云霄宇霁

上一篇主要详细介绍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>
log4net basic
  • 配置详解: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");
        }
    }
LogService
  • 可以通过构造函数的方式依赖注入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();
Program.cs

启动程序可以看到生成了一个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()}");
    }
}
ClientIPPatternConvert
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]}");
    }
}
UserNamePatternConvert

 接下来需要将自定义的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));
    }
}
DemoPatterLayout

修改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>
Customize layout conversation

在LogService中向log4net添加IP及UserName属性值

LogicalThreadContext.Properties[LoggerProperty.Ip] = "127.0.0.1";
LogicalThreadContext.Properties[LoggerProperty.UserName] = "panda";
log4net property value

运行结果

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);
}
AbstractLayout

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; }
}
Base Logger

定义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));
    }
}
ELKWebLayout

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; }
}
Web Logger

在log4net.config中指定自定义的layout

<layout type ="Log4netDemo.ELKWebLayout, Log4netDemo"/>

给log4net自定义属性赋值

LogicalThreadContext.Properties[LoggerProperty.Ip] = "127.0.0.1";
LogicalThreadContext.Properties[LoggerProperty.UserName] = "panda";
log4net property value

运行结果

 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>
Multiple Logger Appender

在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"/>
FATAL Filter

b.记录Level范围在INFO和ERROR之间的日志信息

<filter type="log4net.Filter.LevelMatchFilter">
     <levelMax value ="ERROR"/>
     <levelMin value ="INFO"/>
</filter>
<filter type="log4net.Filter.DenyAllFilter"/>
Level Range

 Filter执行原理是顺序执行,如果满足上一个Filter则日志会被记录;如果不满足上一个Filter会继续匹配下一个Filter。

最后附上完整的代码结构

 如有任何问题,请留言,谢谢!!!