日志在大型软件开发过程中扮演着不可或缺的角色,开发者经常使用日志对系统进行调试和监控。
.NET平台上有多种流行的日志库,本文对log4net和Serilog两种日志库做简要介绍,从使用角度来说,后者更加易用和强大。
log4net

配置文件示例
log4net支持种类多样的日志输出方式,都可以在配置文件中进行设置,一个实用例子如下:
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| <?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net" /> </configSections>
<log4net> <root> <level value="ALL"/> <appender-ref ref="ConsoleAppender"/> <appender-ref ref="RollingLogFileAppender4DEBUG"/> <appender-ref ref="RollingLogFileAppender4INFO"/> <appender-ref ref="RollingLogFileAppender4ERROR"/> </root>
<logger name="TestLog.Logger"/>
<appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender"> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="[%-5level Thread-%thread] [%date] - %message%newline" /> </layout> </appender>
<appender name="RollingLogFileAppender4DEBUG" type="log4net.Appender.RollingFileAppender"> <file value="logs/DEBUG.log" /> <appendToFile value="true" /> <rollingStyle value="Composite" /> <maxSizeRollBackups value="10" /> <maximumFileSize value="1MB" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%date [%thread] %-5level %logger - %message%newline" /> </layout> <filter type="log4net.Filter.LevelRangeFilter"> <levelMin value="DEBUG" /> <levelMax value="DEBUG" /> </filter> </appender>
<appender name="RollingLogFileAppender4INFO" type="log4net.Appender.RollingFileAppender"> <file value="logs/INFO.log" /> <appendToFile value="true" /> <rollingStyle value="Composite" /> <maxSizeRollBackups value="10" /> <maximumFileSize value="1MB" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%date [%thread] %-5level %logger - %message%newline" /> </layout> <filter type="log4net.Filter.LevelRangeFilter"> <levelMin value="INFO" /> <levelMax value="INFO" /> </filter> </appender> <appender name="RollingLogFileAppender4ERROR" type="log4net.Appender.RollingFileAppender"> <file value="logs/ERROR.log" /> <appendToFile value="true" /> <rollingStyle value="Composite" /> <maxSizeRollBackups value="10" /> <maximumFileSize value="1MB" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%date [%thread] %-5level %logger - %message%newline" /> </layout> <filter type="log4net.Filter.LevelRangeFilter"> <levelMin value="ERROR" /> <levelMax value="ERROR" /> </filter> </appender> </log4net> </configuration>
|
以上日志配置文件添加了四个appender,其中ConsoleAppender可输出所有级别的日志,另外三个是滚动文件输出器,分别针对DEBUG、INFO和ERROR级别的日志。
appender
appender用于定义日志输出的格式,包括以下各种类型,每种类型都有各自的定义方式,定义appender是使用log4net日志库配置文件的关键。

root和logger
理解root和logger的关系是深入理解log4net库的核心:
- root是最基础的日志配置,logger在此基础上进行更细粒度的配置,后者可以覆盖前者的日志输出级别和日志输出方式。
- 如果没有为logger指定
additivity=false
,那么logger输出日志后,会向上传递日志事件。
- 例子1:DEBUG及以上的事件会通过ConsoleAppender2输出,INFO及以上的事件也会向上传递到root,通过ConsoleAppender输出:
1 2 3 4 5 6 7 8 9
| <root> <level value="INFO"/> <appender-ref ref="ConsoleAppender"/> </root>
<logger name="TestLog.Logger"> <level value="DEBUG"/> <appender-ref ref="ConsoleAppender2"/> </logger>
|
- 例子2:DEBUG及以上的事件只会通过ConsoleAppender2输出,因为
additivity="false"
把日志向上传递的线路阻断了:
1 2 3 4 5 6 7 8 9
| <root> <level value="INFO"/> <appender-ref ref="ConsoleAppender"/> </root>
<logger name="TestLog.Logger" additivity="false"> <level value="DEBUG"/> <appender-ref ref="ConsoleAppender2"/> </logger>
|
日志工具类
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
| public static class Logger { private static readonly ILog _log=LogManager.GetLogger("TestLog.Logger");
static Logger() { var location = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); XmlConfigurator.ConfigureAndWatch(new FileInfo(Path.Combine(location, "log4net.config"))); }
public static void Debug(string message) { _log.Debug(message); }
public static void Info(string message) { _log.Info(message); }
public static void Warn(string message) { _log.Warn(message); }
public static void Error(string message) { _log.Error(message); }
public static void Fatal(string message) { _log.Fatal(message); } }
|
写日志
1 2 3 4 5 6
| static void Main(string[] args) { Logger.Debug("debug1"); Logger.Info("I am LiHao"); Logger.Info("Hello, world!"); Console.ReadKey(); }
|
Serilog
使用Serilog实现上述log4net的日志功能。

日志器配置
与前者不同,Serilog采用API的方式对日志器进行配置,可以为项目集成多种日志器,以下模仿上文的功能,配置控制台和文件两种日志器:
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 42 43 44 45 46 47 48 49 50 51 52 53
| public static class Logger { static Logger() { var template = "[{Level:u3} {ThreadId}] [{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] {Message:lj}{NewLine}{Exception}"; Log.Logger = new LoggerConfiguration() .Enrich.With(new ThreadIdEnricher()) .MinimumLevel.Debug() .WriteTo.Console(outputTemplate: template) .WriteTo.Map( keySelector: e => e.Level, configure: (level, config) => config.File( path: $"logs/{level}-.txt", restrictedToMinimumLevel: level, rollingInterval: RollingInterval.Day, outputTemplate: template, rollOnFileSizeLimit: true, fileSizeLimitBytes: 1 * 1024 * 1024, retainedFileCountLimit: 10, flushToDiskInterval: TimeSpan.FromSeconds(5), shared: true) ) .CreateLogger(); }
public static void Debug(string message) { Log.Debug(message); }
public static void Information(string message) { Log.Information(message); }
public static void Warning(string message) { Log.Warning(message); }
public static void Error(string message) { Log.Error(message); }
public static void Fatal(string message) { Log.Fatal(message); }
#region 提供其它属性 private class ThreadIdEnricher : ILogEventEnricher { public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty( "ThreadId", Thread.CurrentThread.ManagedThreadId)); } } #endregion }
|
- 可以定义实现了
ILogEventEnricher
的事件改进器,为日志器添加新的属性,以上ThreadIdEnricher
可以为日志器添加线程ID属性,从而进行输出。
写日志
1 2 3 4 5 6 7 8 9
| static void Main() { for (int i = 0; i < 10; i++) { Logger.Debug("Debug"); Logger.Information("Information"); Logger.Warning("Warning"); Logger.Error("Error"); Logger.Fatal("Fatal"); } }
|
其它一些技巧
记录方法运行时间
可以使用MethodTimer.Fody库,可以为方法植入输出方法执行耗时的额外代码。
1 2 3 4 5
| public static class MethodTimeLogger { public static void Log(MethodBase methodBase, long milliseconds) { Logger.Information($"{methodBase.Name} 执行耗时: {milliseconds}ms"); } }
|
- 为方法添加[Time]注解,就能为方法植入耗时统计的功能
1 2 3 4 5 6 7
| [Time] static void TestMethod1() { double res = 0.0; for (int i = 0; i < 10000000; i++) { res += i; } }
|