简介:log4net-1.2.10 是 Apache 基金会维护的经典 .NET 日志记录框架,面向需要在 ASP.NET、Windows 服务、WPF 或 WinForms 项目中构建稳定日志体系的开发者,尤其适合希望深入源码理解其运行机制的中高级工程师。压缩包为 rar 格式,整体约 6.79MB,内含源码与 DLL 文件,源码便于研读 Appender、Layout 等核心实现,DLL 可直接在项目中引用,省去自行编译的步骤。资源围绕配置灵活性、多级别日志、可扩展性与线程安全等特性展开,涵盖 XML 配置、ILog 接口调用、自定义输出端与格式等关键知识点,并配有示例代码与应用场景说明,可帮助读者快速完成日志级别、输出目标与格式的落地配置。目前已有 270 人学习下载,适合对照源码与文档查漏补缺,提升项目的可维护性与排错效率。
1. 为什么一个 1.2.10 版本的日志库,至今还在被反复翻出来
如果你维护过 .NET Framework 4.0 时代的老系统,大概率在packages.config或lib目录里见过log4net.dll,版本号停在 1.2.10。这个版本发布于 2011 年前后,是 log4net 进入 Apache 基金会后比较稳定的一个分支,支持 .NET Framework 2.0 到 4.0,也是很多老项目里唯一能直接引用的日志组件。它解决的问题很具体:把日志输出从Console.WriteLine和手写StreamWriter里解放出来,用配置驱动的方式决定日志写到哪里、按什么格式写、什么级别才写。适合谁?适合还在维护 WinForm、ASP.NET WebForms、WCF 服务或者老式 Windows 服务的人,尤其是那些不能随便升级依赖、只能靠一个稳定版本撑住整套日志链路的场景。下面按“它是什么、怎么配、坑在哪、怎么验证”的顺序拆开讲。
2. log4net 1.2.10 的核心模型与最小可用配置
2.1 三个对象撑起整套日志链路
log4net 的模型不复杂,但很多人第一次配的时候会被 XML 里的层级关系绕晕。核心只有三个东西:Logger、Appender、Layout。Logger 负责按名字和级别接收日志调用,Appender 负责把日志送到目的地,Layout 负责决定每条日志长什么样。三者通过配置文件里的<root>和<logger>节点串起来。
Logger 的命名是层级化的,比如MyApp.Data会继承MyApp的配置。级别从低到高是 ALL、DEBUG、INFO、WARN、ERROR、FATAL、OFF。一个 Logger 如果设成 INFO,那 DEBUG 级别的调用会被直接丢弃,不会走到 Appender。Appender 可以挂多个,比如同时写文件和写数据库。Layout 最常用的是PatternLayout,用转换字符控制输出格式。
1.2.10 这个版本对 .NET Framework 2.0 到 4.0 的兼容性比较稳,没有引入后面版本里一些依赖变化。常见做法是直接引用log4net.dll,然后在AssemblyInfo.cs里加一行[assembly: log4net.Config.XmlConfigurator(Watch = true)],让程序启动时自动读配置。如果不想改 AssemblyInfo,也可以在Main方法里手动调XmlConfigurator.Configure()。
2.2 最小可用配置:控制台加滚动文件
下面这份配置可以直接抄进App.config或Web.config的<configuration>节点下。它定义了两个 Appender:一个写控制台,一个写按日期滚动的文件。根 Logger 设成 DEBUG,意味着所有级别都会输出。
<configSections> <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" /> </configSections> <log4net> <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender"> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%date [%thread] %-5level %logger - %message%newline" /> </layout> </appender> <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender"> <file value="logs/app.log" /> <appendToFile value="true" /> <rollingStyle value="Date" /> <datePattern value="yyyyMMdd" /> <maxSizeRollBackups value="30" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%date [%thread] %-5level %logger - %message%newline" /> </layout> </appender> <root> <level value="DEBUG" /> <appender-ref ref="ConsoleAppender" /> <appender-ref ref="RollingFileAppender" /> </root> </log4net>configSections必须放在<configuration>的第一个子节点位置,否则 .NET 配置系统会直接抛异常。RollingFileAppender的rollingStyle设成Date时,datePattern决定文件名的日期后缀,maxSizeRollBackups控制保留多少个历史文件。appendToFile为 true 表示追加而不是覆盖。conversionPattern里的%date默认格式是 ISO8601,%thread是线程号,%-5level表示级别左对齐占 5 个字符,%logger是 Logger 名字,%message是日志内容,%newline是换行。
代码里获取 Logger 的标准写法是每个类一个静态字段:
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);用DeclaringType的好处是 Logger 名字自动等于类的全名,后面按命名空间调级别时不用手动维护字符串。如果类在MyApp.Data命名空间下,那 Logger 名字就是MyApp.Data.UserRepository,可以在配置里单独给MyApp.Data设成 WARN,把数据层的 DEBUG 日志关掉。
2.3 按命名空间调级别:别让所有日志都涌出来
根 Logger 设成 DEBUG 在开发期没问题,但生产环境如果所有类都输出 DEBUG,文件会涨得很快。常见做法是把根设成 INFO 或 WARN,然后给需要排查的命名空间单独开 DEBUG。
<logger name="MyApp.Data"> <level value="DEBUG" /> </logger> <logger name="MyApp.Web"> <level value="WARN" /> </logger> <root> <level value="INFO" /> <appender-ref ref="RollingFileAppender" /> </root><logger>节点的name是前缀匹配,MyApp.Data会同时匹配MyApp.Data.UserRepository和MyApp.Data.OrderDao。additivity默认是 true,意味着日志除了走自己的 Appender,还会往父级 Logger 传。如果不想重复输出,可以设additivity="false"。这个属性在 1.2.10 里是支持的,但很多人配的时候会忽略,结果同一条日志在控制台出现两次。
3. 把日志写进数据库和按大小滚动:两个高频需求的落地细节
3.1 AdoNetAppender 写 SQL Server 的完整参数
把日志写数据库在审计和集中排查场景里很常见。1.2.10 自带AdoNetAppender,支持通过connectionType和connectionString指定数据库。下面这份配置写的是 SQL Server,表结构需要提前建好。
CREATE TABLE Log ( Id INT IDENTITY(1,1) PRIMARY KEY, LogDate DATETIME NOT NULL, Thread VARCHAR(255), LogLevel VARCHAR(50), Logger VARCHAR(255), Message VARCHAR(4000), Exception TEXT );对应的 Appender 配置:
<appender name="AdoNetAppender" type="log4net.Appender.AdoNetAppender"> <bufferSize value="10" /> <connectionType value="System.Data.SqlClient.SqlConnection, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" /> <connectionString value="Data Source=.;Initial Catalog=LogDb;Integrated Security=True" /> <commandText value="INSERT INTO Log (LogDate, Thread, LogLevel, Logger, Message, Exception) VALUES (@log_date, @thread, @log_level, @logger, @message, @exception)" /> <parameter> <parameterName value="@log_date" /> <dbType value="DateTime" /> <layout type="log4net.Layout.RawTimeStampLayout" /> </parameter> <parameter> <parameterName value="@thread" /> <dbType value="String" /> <size value="255" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%thread" /> </layout> </parameter> <parameter> <parameterName value="@log_level" /> <dbType value="String" /> <size value="50" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%level" /> </layout> </parameter> <parameter> <parameterName value="@logger" /> <dbType value="String" /> <size value="255" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%logger" /> </layout> </parameter> <parameter> <parameterName value="@message" /> <dbType value="String" /> <size value="4000" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%message" /> </layout> </parameter> <parameter> <parameterName value="@exception" /> <dbType value="String" /> <size value="2000" /> <layout type="log4net.Layout.ExceptionLayout" /> </parameter> </appender>bufferSize设成 10 表示攒够 10 条才批量提交一次,设成 0 或 1 表示每条都立即写。生产环境如果日志量大,bufferSize 可以调到 50 到 100,但要注意进程崩溃时缓冲区里没提交的日志会丢。connectionType的版本号要和目标机器上的System.Data匹配,1.2.10 时代常见的是Version=2.0.0.0,如果项目跑在 .NET 4.0 上,改成Version=4.0.0.0更稳。RawTimeStampLayout直接输出 DateTime 对象,不需要格式化字符串,比用 PatternLayout 转%date再让数据库解析更可靠。
3.2 RollingFileAppender 按大小滚动的参数组合
按日期滚动适合每天一个文件的场景,但如果单日日志量很大,一个文件可能涨到几个 GB,打开都费劲。按大小滚动更实用,配置里把rollingStyle改成Size,配合maximumFileSize和maxSizeRollBackups。
<appender name="SizeRollingAppender" type="log4net.Appender.RollingFileAppender"> <file value="logs/app.log" /> <appendToFile value="true" /> <rollingStyle value="Composite" /> <datePattern value="yyyyMMdd" /> <maximumFileSize value="10MB" /> <maxSizeRollBackups value="20" /> <staticLogFileName value="true" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%date [%thread] %-5level %logger - %message%newline" /> </layout> </appender>rollingStyle设成Composite表示同时按日期和大小滚动,这是实际项目里最常用的组合。maximumFileSize支持 KB、MB、GB 单位,写10MB比写10485760可读性好。maxSizeRollBackups是历史文件数量上限,超过后最旧的文件会被删除。staticLogFileName设成 true 表示当前日志始终写app.log,历史文件带日期和序号后缀;设成 false 则当前日志也带日期后缀。这个参数在 1.2.10 里行为比较稳定,但要注意如果file路径里的目录不存在,Appender 不会自动创建,需要在程序启动时手动建目录。
3.3 代码里怎么打日志才不丢上下文
配置只是管道,真正决定日志有没有用的还是调用方式。常见做法是在 catch 块里把异常对象作为第二个参数传进去,让ExceptionLayout能拿到堆栈。
try { ProcessOrder(orderId); } catch (Exception ex) { log.Error($"处理订单 {orderId} 失败", ex); throw; }log.Error的重载里,第一个参数是消息,第二个是异常对象。如果只写log.Error(ex.Message),堆栈就丢了,后面排查只能看到一行消息。另外,字符串插值在 1.2.10 里是支持的,因为它是 C# 编译器特性,不依赖 log4net 版本。但如果日志级别没开,插值仍然会执行,有轻微性能开销。对性能敏感的循环里,可以先判断log.IsDebugEnabled再拼字符串。
4. 避坑:1.2.10 在老项目里最容易翻车的五个点
4.1 配置文件不生效,日志一条都不出
现象是程序跑起来没有任何日志输出,也不报错。原因通常是configSections节点位置不对,或者AssemblyInfo.cs里的XmlConfigurator没加。解决方法是先确认<configSections>是<configuration>的第一个子节点,然后检查[assembly: log4net.Config.XmlConfigurator(Watch = true)]是否存在。如果用的是 Web 项目,配置文件应该是Web.config而不是App.config。还有一个隐蔽情况:如果项目引用的log4net.dll版本和配置文件里configSections的type不匹配,配置系统会静默忽略整个 log4net 节。
4.2 日志文件被占用,无法删除或重命名
现象是程序运行期间想清理日志文件,提示文件被另一个进程占用。原因是RollingFileAppender默认以共享读的方式打开文件,但 Windows 下如果其他程序也用独占方式打开同一个文件,就会冲突。解决办法是不要在程序运行时手动操作日志文件,需要清理就停掉进程再删。如果必须在线清理,可以把lockingModel设成log4net.Appender.FileAppender+MinimalLock,这样 log4net 只在写的时候加锁,但会牺牲一些性能。
4.3 数据库 Appender 报连接池耗尽
现象是运行一段时间后开始抛Timeout expired,日志写不进数据库。原因是AdoNetAppender每次批量提交都会开新连接,如果bufferSize设得太小、日志量又大,连接池很快被打满。解决办法是把bufferSize调大,比如 50 到 100,减少提交频率;同时确认连接字符串里没有设Max Pool Size过小。另外,AdoNetAppender在 1.2.10 里没有内置的重试机制,数据库短暂不可用时日志会直接丢,对可靠性要求高的场景需要自己包一层。
4.4 多线程下日志时间戳乱序
现象是日志文件里时间戳不是递增的,后写的日志时间反而更早。原因是多个线程同时调用 Appender,PatternLayout里的%date取的是日志事件创建时间,但写入顺序受锁竞争影响。这在 1.2.10 里是正常行为,因为 Appender 内部有锁保证不写乱,但不保证严格按时间排序。如果排查问题时依赖时间顺序,可以在conversionPattern里加上%thread,按线程号分组看。真正需要严格顺序的场景,常见做法是加一个单线程的日志队列,但那就超出 log4net 本身的范围了。
4.5 发布到服务器后路径不对
现象是本地开发时日志正常写到logs/app.log,发布到 IIS 或 Windows 服务后文件不知道去哪了。原因是相对路径的基准目录取决于当前工作目录,IIS 下通常是%SystemRoot%\System32\inetsrv,Windows 服务下是C:\Windows\System32。解决办法是在配置文件里用绝对路径,或者在程序启动时用log4net.Util.SystemInfo拼出基于AppDomain.CurrentDomain.BaseDirectory的路径。更省事的做法是在Global.asax的Application_Start里调XmlConfigurator.Configure之前先Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory)。
5. 验证配置是否生效:三个不用重启就能查的技巧
5.1 用 log4net 内部调试输出看配置加载过程
log4net 自己有一套内部日志,默认不输出。在App.config的<appSettings>里加两行就能打开:
<appSettings> <add key="log4net.Internal.Debug" value="true" /> <add key="log4net.Internal.DebugFile" value="log4net-internal.log" /> </appSettings>程序启动后,log4net-internal.log里会记录每个 Appender 的创建、每个 Logger 的级别设置、以及配置解析时的警告。如果某个 Appender 没生效,这里通常能看到No appender named ...或者Could not create appender之类的提示。这个文件在排查配置问题时比猜快得多,我一般会在第一次部署到新环境时打开,确认无误后再关掉。
5.2 用单元测试验证日志输出
如果项目有测试工程,可以写一个简单的测试确认日志真的写到了目标位置。下面这个例子用内存 Appender 捕获日志,不依赖文件系统。
[Test] public void Log_Info_WritesToMemoryAppender() { var memoryAppender = new log4net.Appender.MemoryAppender(); memoryAppender.Layout = new log4net.Layout.PatternLayout("%message"); memoryAppender.ActivateOptions(); var hierarchy = (log4net.Repository.Hierarchy.Hierarchy)log4net.LogManager.GetRepository(); hierarchy.Root.AddAppender(memoryAppender); hierarchy.Root.Level = log4net.Core.Level.Info; hierarchy.Configured = true; var log = log4net.LogManager.GetLogger("TestLogger"); log.Info("hello"); var events = memoryAppender.GetEvents(); Assert.AreEqual(1, events.Length); Assert.AreEqual("hello", events[0].RenderedMessage); }MemoryAppender是 1.2.10 自带的,不需要额外引用。ActivateOptions必须在添加之前调用,否则 Layout 不生效。hierarchy.Configured = true告诉 log4net 配置已经完成,可以开始接收日志。这个测试的好处是不碰文件也不碰数据库,跑得快,适合放在 CI 里防止配置被误改。
5.3 用 PowerShell 快速检查日志文件滚动是否符合预期
部署到服务器后,如果怀疑滚动策略没生效,可以用 PowerShell 看一眼目录里的文件列表和大小。
Get-ChildItem -Path "C:\App\logs" -Filter "app.log*" | Sort-Object LastWriteTime -Descending | Select-Object Name, Length, LastWriteTime | Format-Table -AutoSize如果rollingStyle设的是Composite,应该能看到app.log加上带日期和序号的备份文件。Length列能确认单个文件是否超过了maximumFileSize。如果发现只有一个文件在无限增长,大概率是maximumFileSize写成了10而不是10MB,log4net 会把它当成 10 字节,但实际行为可能不符合预期。这个检查我每次上线新服务都会跑一遍,比等出问题再翻配置快。
5.4 一个我自己的习惯
从那以后我每次给老项目配 log4net,都会先在测试环境把log4net.Internal.Debug打开跑一遍,确认所有 Appender 都按预期创建,再关掉内部调试上生产。这个习惯帮我省过好几次“配置看起来对但就是不写日志”的排查时间。希望帮到你。
本文还有配套的精品资源,点击获取